import {
  BackSide,
  BoxGeometry,
  BufferAttribute,
  BufferGeometry,
  ClampToEdgeWrapping,
  Color,
  ConeGeometry,
  CylinderGeometry,
  DataTexture,
  DoubleSide,
  FileLoader,
  Float32BufferAttribute,
  FrontSide,
  Group,
  LineBasicMaterial,
  LineSegments,
  Loader,
  LoaderUtils,
  Mesh,
  MeshBasicMaterial,
  MeshPhongMaterial,
  Object3D,
  Points,
  PointsMaterial,
  Quaternion,
  RepeatWrapping,
  RGBAFormat,
  RGBFormat,
  Scene,
  ShapeUtils,
  SphereGeometry,
  TextureLoader,
  Vector2,
  Vector3
} from '../../three.module.js';
import chevrotain from '../libs/chevrotain.module.min.js';


class VRMLLoader extends Loader {

  constructor(manager) {

    super(manager);

    // dependency check

    if (typeof chevrotain === 'undefined') { // eslint-disable-line no-undef

      throw Error('THREE.VRMLLoader: External library chevrotain.min.js required.');

    }

  }

  load(url, onLoad, onProgress, onError) {

    const scope = this;

    const path = (scope.path === '') ? LoaderUtils.extractUrlBase(url) : scope.path;

    const loader = new FileLoader(scope.manager);
    loader.setPath(scope.path);
    loader.setRequestHeader(scope.requestHeader);
    loader.setWithCredentials(scope.withCredentials);
    loader.load(url, function (text) {

      try {

        onLoad(scope.parse(text, path));

      } catch (e) {

        if (onError) {

          onError(e);

        } else {

          console.error(e);

        }

        scope.manager.itemError(url);

      }

    }, onProgress, onError);

  }

  parse(data, path) {

    const nodeMap = {};

    function generateVRMLTree(data) {

      // create lexer, parser and visitor

      const tokenData = createTokens();

      const lexer = new VRMLLexer(tokenData.tokens);
      const parser = new VRMLParser(tokenData.tokenVocabulary);
      const visitor = createVisitor(parser.getBaseCstVisitorConstructor());

      // lexing

      const lexingResult = lexer.lex(data);
      parser.input = lexingResult.tokens;

      // parsing

      const cstOutput = parser.vrml();

      if (parser.errors.length > 0) {

        console.error(parser.errors);

        throw Error('THREE.VRMLLoader: Parsing errors detected.');

      }

      // actions

      const ast = visitor.visit(cstOutput);

      return ast;

    }

    function createTokens() {

      const createToken = chevrotain.createToken; // eslint-disable-line no-undef

      // from http://gun.teipir.gr/VRML-amgem/spec/part1/concepts.html#SyntaxBasics

      const RouteIdentifier = createToken({
        name: 'RouteIdentifier',
        pattern: /[^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*[\.][^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*/
      });
      const Identifier = createToken({
        name: 'Identifier',
        pattern: /[^\x30-\x39\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d][^\0-\x20\x22\x27\x23\x2b\x2c\x2d\x2e\x5b\x5d\x5c\x7b\x7d]*/,
        longer_alt: RouteIdentifier
      });

      // from http://gun.teipir.gr/VRML-amgem/spec/part1/nodesRef.html

      const nodeTypes = [
        'Anchor', 'Billboard', 'Collision', 'Group', 'Transform', // grouping nodes
        'Inline', 'LOD', 'Switch', // special groups
        'AudioClip', 'DirectionalLight', 'PointLight', 'Script', 'Shape', 'Sound', 'SpotLight', 'WorldInfo', // common nodes
        'CylinderSensor', 'PlaneSensor', 'ProximitySensor', 'SphereSensor', 'TimeSensor', 'TouchSensor', 'VisibilitySensor', // sensors
        'Box', 'Cone', 'Cylinder', 'ElevationGrid', 'Extrusion', 'IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', // geometries
        'Color', 'Coordinate', 'Normal', 'TextureCoordinate', // geometric properties
        'Appearance', 'FontStyle', 'ImageTexture', 'Material', 'MovieTexture', 'PixelTexture', 'TextureTransform', // appearance
        'ColorInterpolator', 'CoordinateInterpolator', 'NormalInterpolator', 'OrientationInterpolator', 'PositionInterpolator', 'ScalarInterpolator', // interpolators
        'Background', 'Fog', 'NavigationInfo', 'Viewpoint', // bindable nodes
        'Text' // Text must be placed at the end of the regex so there are no matches for TextureTransform and TextureCoordinate
      ];

      //

      const Version = createToken({
        name: 'Version',
        pattern: /#VRML.*/,
        longer_alt: Identifier
      });

      const NodeName = createToken({
        name: 'NodeName',
        pattern: new RegExp(nodeTypes.join('|')),
        longer_alt: Identifier
      });

      const DEF = createToken({
        name: 'DEF',
        pattern: /DEF/,
        longer_alt: Identifier
      });

      const USE = createToken({
        name: 'USE',
        pattern: /USE/,
        longer_alt: Identifier
      });

      const ROUTE = createToken({
        name: 'ROUTE',
        pattern: /ROUTE/,
        longer_alt: Identifier
      });

      const TO = createToken({
        name: 'TO',
        pattern: /TO/,
        longer_alt: Identifier
      });

      //

      const StringLiteral = createToken({
        name: 'StringLiteral',
        pattern: /"(?:[^\\"\n\r]|\\[bfnrtv"\\/]|\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])*"/
      });
      const HexLiteral = createToken({name: 'HexLiteral', pattern: /0[xX][0-9a-fA-F]+/});
      const NumberLiteral = createToken({name: 'NumberLiteral', pattern: /[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/});
      const TrueLiteral = createToken({name: 'TrueLiteral', pattern: /TRUE/});
      const FalseLiteral = createToken({name: 'FalseLiteral', pattern: /FALSE/});
      const NullLiteral = createToken({name: 'NullLiteral', pattern: /NULL/});
      const LSquare = createToken({name: 'LSquare', pattern: /\[/});
      const RSquare = createToken({name: 'RSquare', pattern: /]/});
      const LCurly = createToken({name: 'LCurly', pattern: /{/});
      const RCurly = createToken({name: 'RCurly', pattern: /}/});
      const Comment = createToken({
        name: 'Comment',
        pattern: /#.*/,
        group: chevrotain.Lexer.SKIPPED // eslint-disable-line no-undef
      });

      // commas, blanks, tabs, newlines and carriage returns are whitespace characters wherever they appear outside of string fields

      const WhiteSpace = createToken({
        name: 'WhiteSpace',
        pattern: /[ ,\s]/,
        group: chevrotain.Lexer.SKIPPED // eslint-disable-line no-undef
      });

      const tokens = [
        WhiteSpace,
        // keywords appear before the Identifier
        NodeName,
        DEF,
        USE,
        ROUTE,
        TO,
        TrueLiteral,
        FalseLiteral,
        NullLiteral,
        // the Identifier must appear after the keywords because all keywords are valid identifiers
        Version,
        Identifier,
        RouteIdentifier,
        StringLiteral,
        HexLiteral,
        NumberLiteral,
        LSquare,
        RSquare,
        LCurly,
        RCurly,
        Comment
      ];

      const tokenVocabulary = {};

      for (let i = 0, l = tokens.length; i < l; i++) {

        const token = tokens[i];

        tokenVocabulary[token.name] = token;

      }

      return {tokens: tokens, tokenVocabulary: tokenVocabulary};

    }


    function createVisitor(BaseVRMLVisitor) {

      // the visitor is created dynmaically based on the given base class

      function VRMLToASTVisitor() {

        BaseVRMLVisitor.call(this);

        this.validateVisitor();

      }

      VRMLToASTVisitor.prototype = Object.assign(Object.create(BaseVRMLVisitor.prototype), {

        constructor: VRMLToASTVisitor,

        vrml: function (ctx) {

          const data = {
            version: this.visit(ctx.version),
            nodes: [],
            routes: []
          };

          for (let i = 0, l = ctx.node.length; i < l; i++) {

            const node = ctx.node[i];

            data.nodes.push(this.visit(node));

          }

          if (ctx.route) {

            for (let i = 0, l = ctx.route.length; i < l; i++) {

              const route = ctx.route[i];

              data.routes.push(this.visit(route));

            }

          }

          return data;

        },

        version: function (ctx) {

          return ctx.Version[0].image;

        },

        node: function (ctx) {

          const data = {
            name: ctx.NodeName[0].image,
            fields: []
          };

          if (ctx.field) {

            for (let i = 0, l = ctx.field.length; i < l; i++) {

              const field = ctx.field[i];

              data.fields.push(this.visit(field));

            }

          }

          // DEF

          if (ctx.def) {

            data.DEF = this.visit(ctx.def[0]);

          }

          return data;

        },

        field: function (ctx) {

          const data = {
            name: ctx.Identifier[0].image,
            type: null,
            values: null
          };

          let result;

          // SFValue

          if (ctx.singleFieldValue) {

            result = this.visit(ctx.singleFieldValue[0]);

          }

          // MFValue

          if (ctx.multiFieldValue) {

            result = this.visit(ctx.multiFieldValue[0]);

          }

          data.type = result.type;
          data.values = result.values;

          return data;

        },

        def: function (ctx) {

          return (ctx.Identifier || ctx.NodeName)[0].image;

        },

        use: function (ctx) {

          return {USE: (ctx.Identifier || ctx.NodeName)[0].image};

        },

        singleFieldValue: function (ctx) {

          return processField(this, ctx);

        },

        multiFieldValue: function (ctx) {

          return processField(this, ctx);

        },

        route: function (ctx) {

          const data = {
            FROM: ctx.RouteIdentifier[0].image,
            TO: ctx.RouteIdentifier[1].image
          };

          return data;

        }

      });

      function processField(scope, ctx) {

        const field = {
          type: null,
          values: []
        };

        if (ctx.node) {

          field.type = 'node';

          for (let i = 0, l = ctx.node.length; i < l; i++) {

            const node = ctx.node[i];

            field.values.push(scope.visit(node));

          }

        }

        if (ctx.use) {

          field.type = 'use';

          for (let i = 0, l = ctx.use.length; i < l; i++) {

            const use = ctx.use[i];

            field.values.push(scope.visit(use));

          }

        }

        if (ctx.StringLiteral) {

          field.type = 'string';

          for (let i = 0, l = ctx.StringLiteral.length; i < l; i++) {

            const stringLiteral = ctx.StringLiteral[i];

            field.values.push(stringLiteral.image.replace(/'|"/g, ''));

          }

        }

        if (ctx.NumberLiteral) {

          field.type = 'number';

          for (let i = 0, l = ctx.NumberLiteral.length; i < l; i++) {

            const numberLiteral = ctx.NumberLiteral[i];

            field.values.push(parseFloat(numberLiteral.image));

          }

        }

        if (ctx.HexLiteral) {

          field.type = 'hex';

          for (let i = 0, l = ctx.HexLiteral.length; i < l; i++) {

            const hexLiteral = ctx.HexLiteral[i];

            field.values.push(hexLiteral.image);

          }

        }

        if (ctx.TrueLiteral) {

          field.type = 'boolean';

          for (let i = 0, l = ctx.TrueLiteral.length; i < l; i++) {

            const trueLiteral = ctx.TrueLiteral[i];

            if (trueLiteral.image === 'TRUE') field.values.push(true);

          }

        }

        if (ctx.FalseLiteral) {

          field.type = 'boolean';

          for (let i = 0, l = ctx.FalseLiteral.length; i < l; i++) {

            const falseLiteral = ctx.FalseLiteral[i];

            if (falseLiteral.image === 'FALSE') field.values.push(false);

          }

        }

        if (ctx.NullLiteral) {

          field.type = 'null';

          ctx.NullLiteral.forEach(function () {

            field.values.push(null);

          });

        }

        return field;

      }

      return new VRMLToASTVisitor();

    }

    function parseTree(tree) {

      // console.log( JSON.stringify( tree, null, 2 ) );

      const nodes = tree.nodes;
      const scene = new Scene();

      // first iteration: build nodemap based on DEF statements

      for (let i = 0, l = nodes.length; i < l; i++) {

        const node = nodes[i];

        buildNodeMap(node);

      }

      // second iteration: build nodes

      for (let i = 0, l = nodes.length; i < l; i++) {

        const node = nodes[i];
        const object = getNode(node);

        if (object instanceof Object3D) scene.add(object);

        if (node.name === 'WorldInfo') scene.userData.worldInfo = object;

      }

      return scene;

    }

    function buildNodeMap(node) {

      if (node.DEF) {

        nodeMap[node.DEF] = node;

      }

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];

        if (field.type === 'node') {

          const fieldValues = field.values;

          for (let j = 0, jl = fieldValues.length; j < jl; j++) {

            buildNodeMap(fieldValues[j]);

          }

        }


      }

    }


    function getNode(node) {

      // handle case where a node refers to a different one

      if (node.USE) {

        return resolveUSE(node.USE);

      }

      if (node.build !== undefined) return node.build;

      node.build = buildNode(node);

      return node.build;

    }

    // node builder

    function buildNode(node) {

      const nodeName = node.name;
      let build;

      switch (nodeName) {

        case 'Group':
        case 'Transform':
        case 'Collision':
          build = buildGroupingNode(node);
          break;

        case 'Background':
          build = buildBackgroundNode(node);
          break;

        case 'Shape':
          build = buildShapeNode(node);
          break;

        case 'Appearance':
          build = buildAppearanceNode(node);
          break;

        case 'Material':
          build = buildMaterialNode(node);
          break;

        case 'ImageTexture':
          build = buildImageTextureNode(node);
          break;

        case 'PixelTexture':
          build = buildPixelTextureNode(node);
          break;

        case 'TextureTransform':
          build = buildTextureTransformNode(node);
          break;

        case 'IndexedFaceSet':
          build = buildIndexedFaceSetNode(node);
          break;

        case 'IndexedLineSet':
          build = buildIndexedLineSetNode(node);
          break;

        case 'PointSet':
          build = buildPointSetNode(node);
          break;

        case 'Box':
          build = buildBoxNode(node);
          break;

        case 'Cone':
          build = buildConeNode(node);
          break;

        case 'Cylinder':
          build = buildCylinderNode(node);
          break;

        case 'Sphere':
          build = buildSphereNode(node);
          break;

        case 'ElevationGrid':
          build = buildElevationGridNode(node);
          break;

        case 'Extrusion':
          build = buildExtrusionNode(node);
          break;

        case 'Color':
        case 'Coordinate':
        case 'Normal':
        case 'TextureCoordinate':
          build = buildGeometricNode(node);
          break;

        case 'WorldInfo':
          build = buildWorldInfoNode(node);
          break;

        case 'Anchor':
        case 'Billboard':

        case 'Inline':
        case 'LOD':
        case 'Switch':

        case 'AudioClip':
        case 'DirectionalLight':
        case 'PointLight':
        case 'Script':
        case 'Sound':
        case 'SpotLight':

        case 'CylinderSensor':
        case 'PlaneSensor':
        case 'ProximitySensor':
        case 'SphereSensor':
        case 'TimeSensor':
        case 'TouchSensor':
        case 'VisibilitySensor':

        case 'Text':

        case 'FontStyle':
        case 'MovieTexture':

        case 'ColorInterpolator':
        case 'CoordinateInterpolator':
        case 'NormalInterpolator':
        case 'OrientationInterpolator':
        case 'PositionInterpolator':
        case 'ScalarInterpolator':

        case 'Fog':
        case 'NavigationInfo':
        case 'Viewpoint':
          // node not supported yet
          break;

        default:
          console.warn('THREE.VRMLLoader: Unknown node:', nodeName);
          break;

      }

      if (build !== undefined && node.DEF !== undefined && build.hasOwnProperty('name') === true) {

        build.name = node.DEF;

      }

      return build;

    }

    function buildGroupingNode(node) {

      const object = new Group();

      //

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'bboxCenter':
            // field not supported
            break;

          case 'bboxSize':
            // field not supported
            break;

          case 'center':
            // field not supported
            break;

          case 'children':
            parseFieldChildren(fieldValues, object);
            break;

          case 'collide':
            // field not supported
            break;

          case 'rotation':
            const axis = new Vector3(fieldValues[0], fieldValues[1], fieldValues[2]);
            const angle = fieldValues[3];
            object.quaternion.setFromAxisAngle(axis, angle);
            break;

          case 'scale':
            object.scale.set(fieldValues[0], fieldValues[1], fieldValues[2]);
            break;

          case 'scaleOrientation':
            // field not supported
            break;

          case 'translation':
            object.position.set(fieldValues[0], fieldValues[1], fieldValues[2]);
            break;

          case 'proxy':
            // field not supported
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      return object;

    }

    function buildBackgroundNode(node) {

      const group = new Group();

      let groundAngle, groundColor;
      let skyAngle, skyColor;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'groundAngle':
            groundAngle = fieldValues;
            break;

          case 'groundColor':
            groundColor = fieldValues;
            break;

          case 'backUrl':
            // field not supported
            break;

          case 'bottomUrl':
            // field not supported
            break;

          case 'frontUrl':
            // field not supported
            break;

          case 'leftUrl':
            // field not supported
            break;

          case 'rightUrl':
            // field not supported
            break;

          case 'topUrl':
            // field not supported
            break;

          case 'skyAngle':
            skyAngle = fieldValues;
            break;

          case 'skyColor':
            skyColor = fieldValues;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const radius = 10000;

      // sky

      if (skyColor) {

        const skyGeometry = new SphereGeometry(radius, 32, 16);
        const skyMaterial = new MeshBasicMaterial({fog: false, side: BackSide, depthWrite: false, depthTest: false});

        if (skyColor.length > 3) {

          paintFaces(skyGeometry, radius, skyAngle, toColorArray(skyColor), true);
          skyMaterial.vertexColors = true;

        } else {

          skyMaterial.color.setRGB(skyColor[0], skyColor[1], skyColor[2]);

        }

        const sky = new Mesh(skyGeometry, skyMaterial);
        group.add(sky);

      }

      // ground

      if (groundColor) {

        if (groundColor.length > 0) {

          const groundGeometry = new SphereGeometry(radius, 32, 16, 0, 2 * Math.PI, 0.5 * Math.PI, 1.5 * Math.PI);
          const groundMaterial = new MeshBasicMaterial({
            fog: false,
            side: BackSide,
            vertexColors: true,
            depthWrite: false,
            depthTest: false
          });

          paintFaces(groundGeometry, radius, groundAngle, toColorArray(groundColor), false);

          const ground = new Mesh(groundGeometry, groundMaterial);
          group.add(ground);

        }

      }

      // render background group first

      group.renderOrder = -Infinity;

      return group;

    }

    function buildShapeNode(node) {

      const fields = node.fields;

      // if the appearance field is NULL or unspecified, lighting is off and the unlit object color is (0, 0, 0)

      let material = new MeshBasicMaterial({color: 0x000000});
      let geometry;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'appearance':
            if (fieldValues[0] !== null) {

              material = getNode(fieldValues[0]);

            }

            break;

          case 'geometry':
            if (fieldValues[0] !== null) {

              geometry = getNode(fieldValues[0]);

            }

            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      // build 3D object

      let object;

      if (geometry && geometry.attributes.position) {

        const type = geometry._type;

        if (type === 'points') { // points

          const pointsMaterial = new PointsMaterial({color: 0xffffff});

          if (geometry.attributes.color !== undefined) {

            pointsMaterial.vertexColors = true;

          } else {

            // if the color field is NULL and there is a material defined for the appearance affecting this PointSet, then use the emissiveColor of the material to draw the points

            if (material.isMeshPhongMaterial) {

              pointsMaterial.color.copy(material.emissive);

            }

          }

          object = new Points(geometry, pointsMaterial);

        } else if (type === 'line') { // lines

          const lineMaterial = new LineBasicMaterial({color: 0xffffff});

          if (geometry.attributes.color !== undefined) {

            lineMaterial.vertexColors = true;

          } else {

            // if the color field is NULL and there is a material defined for the appearance affecting this IndexedLineSet, then use the emissiveColor of the material to draw the lines

            if (material.isMeshPhongMaterial) {

              lineMaterial.color.copy(material.emissive);

            }

          }

          object = new LineSegments(geometry, lineMaterial);

        } else { // consider meshes

          // check "solid" hint (it's placed in the geometry but affects the material)

          if (geometry._solid !== undefined) {

            material.side = (geometry._solid) ? FrontSide : DoubleSide;

          }

          // check for vertex colors

          if (geometry.attributes.color !== undefined) {

            material.vertexColors = true;

          }

          object = new Mesh(geometry, material);

        }

      } else {

        object = new Object3D();

        // if the geometry field is NULL or no vertices are defined the object is not drawn

        object.visible = false;

      }

      return object;

    }

    function buildAppearanceNode(node) {

      let material = new MeshPhongMaterial();
      let transformData;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'material':
            if (fieldValues[0] !== null) {

              const materialData = getNode(fieldValues[0]);

              if (materialData.diffuseColor) material.color.copy(materialData.diffuseColor);
              if (materialData.emissiveColor) material.emissive.copy(materialData.emissiveColor);
              if (materialData.shininess) material.shininess = materialData.shininess;
              if (materialData.specularColor) material.specular.copy(materialData.specularColor);
              if (materialData.transparency) material.opacity = 1 - materialData.transparency;
              if (materialData.transparency > 0) material.transparent = true;

            } else {

              // if the material field is NULL or unspecified, lighting is off and the unlit object color is (0, 0, 0)

              material = new MeshBasicMaterial({color: 0x000000});

            }

            break;

          case 'texture':
            const textureNode = fieldValues[0];
            if (textureNode !== null) {

              if (textureNode.name === 'ImageTexture' || textureNode.name === 'PixelTexture') {

                material.map = getNode(textureNode);

              } else {

                // MovieTexture not supported yet

              }

            }

            break;

          case 'textureTransform':
            if (fieldValues[0] !== null) {

              transformData = getNode(fieldValues[0]);

            }

            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      // only apply texture transform data if a texture was defined

      if (material.map) {

        // respect VRML lighting model

        if (material.map.__type) {

          switch (material.map.__type) {

            case TEXTURE_TYPE.INTENSITY_ALPHA:
              material.opacity = 1; // ignore transparency
              break;

            case TEXTURE_TYPE.RGB:
              material.color.set(0xffffff); // ignore material color
              break;

            case TEXTURE_TYPE.RGBA:
              material.color.set(0xffffff); // ignore material color
              material.opacity = 1; // ignore transparency
              break;

            default:

          }

          delete material.map.__type;

        }

        // apply texture transform

        if (transformData) {

          material.map.center.copy(transformData.center);
          material.map.rotation = transformData.rotation;
          material.map.repeat.copy(transformData.scale);
          material.map.offset.copy(transformData.translation);

        }

      }

      return material;

    }

    function buildMaterialNode(node) {

      const materialData = {};

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'ambientIntensity':
            // field not supported
            break;

          case 'diffuseColor':
            materialData.diffuseColor = new Color(fieldValues[0], fieldValues[1], fieldValues[2]);
            break;

          case 'emissiveColor':
            materialData.emissiveColor = new Color(fieldValues[0], fieldValues[1], fieldValues[2]);
            break;

          case 'shininess':
            materialData.shininess = fieldValues[0];
            break;

          case 'specularColor':
            materialData.emissiveColor = new Color(fieldValues[0], fieldValues[1], fieldValues[2]);
            break;

          case 'transparency':
            materialData.transparency = fieldValues[0];
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      return materialData;

    }

    function parseHexColor(hex, textureType, color) {

      let value;

      switch (textureType) {

        case TEXTURE_TYPE.INTENSITY:
          // Intensity texture: A one-component image specifies one-byte hexadecimal or integer values representing the intensity of the image
          value = parseInt(hex);
          color.r = value;
          color.g = value;
          color.b = value;
          break;

        case TEXTURE_TYPE.INTENSITY_ALPHA:
          // Intensity+Alpha texture: A two-component image specifies the intensity in the first (high) byte and the alpha opacity in the second (low) byte.
          value = parseInt('0x' + hex.substring(2, 4));
          color.r = value;
          color.g = value;
          color.b = value;
          color.a = parseInt('0x' + hex.substring(4, 6));
          break;

        case TEXTURE_TYPE.RGB:
          // RGB texture: Pixels in a three-component image specify the red component in the first (high) byte, followed by the green and blue components
          color.r = parseInt('0x' + hex.substring(2, 4));
          color.g = parseInt('0x' + hex.substring(4, 6));
          color.b = parseInt('0x' + hex.substring(6, 8));
          break;

        case TEXTURE_TYPE.RGBA:
          // RGBA texture: Four-component images specify the alpha opacity byte after red/green/blue
          color.r = parseInt('0x' + hex.substring(2, 4));
          color.g = parseInt('0x' + hex.substring(4, 6));
          color.b = parseInt('0x' + hex.substring(6, 8));
          color.a = parseInt('0x' + hex.substring(8, 10));
          break;

        default:

      }

    }

    function getTextureType(num_components) {

      let type;

      switch (num_components) {

        case 1:
          type = TEXTURE_TYPE.INTENSITY;
          break;

        case 2:
          type = TEXTURE_TYPE.INTENSITY_ALPHA;
          break;

        case 3:
          type = TEXTURE_TYPE.RGB;
          break;

        case 4:
          type = TEXTURE_TYPE.RGBA;
          break;

        default:

      }

      return type;

    }

    function buildPixelTextureNode(node) {

      let texture;
      let wrapS = RepeatWrapping;
      let wrapT = RepeatWrapping;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'image':
            const width = fieldValues[0];
            const height = fieldValues[1];
            const num_components = fieldValues[2];

            const useAlpha = (num_components === 2 || num_components === 4);
            const textureType = getTextureType(num_components);

            const size = ((useAlpha === true) ? 4 : 3) * (width * height);
            const data = new Uint8Array(size);

            const color = {r: 0, g: 0, b: 0, a: 0};

            for (let j = 3, k = 0, jl = fieldValues.length; j < jl; j++, k++) {

              parseHexColor(fieldValues[j], textureType, color);

              if (useAlpha === true) {

                const stride = k * 4;

                data[stride + 0] = color.r;
                data[stride + 1] = color.g;
                data[stride + 2] = color.b;
                data[stride + 3] = color.a;

              } else {

                const stride = k * 3;

                data[stride + 0] = color.r;
                data[stride + 1] = color.g;
                data[stride + 2] = color.b;

              }

            }

            texture = new DataTexture(data, width, height, (useAlpha === true) ? RGBAFormat : RGBFormat);
            texture.needsUpdate = true;
            texture.__type = textureType; // needed for material modifications
            break;

          case 'repeatS':
            if (fieldValues[0] === false) wrapS = ClampToEdgeWrapping;
            break;

          case 'repeatT':
            if (fieldValues[0] === false) wrapT = ClampToEdgeWrapping;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      if (texture) {

        texture.wrapS = wrapS;
        texture.wrapT = wrapT;

      }

      return texture;

    }

    function buildImageTextureNode(node) {

      let texture;
      let wrapS = RepeatWrapping;
      let wrapT = RepeatWrapping;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'url':
            const url = fieldValues[0];
            if (url) texture = textureLoader.load(url);
            break;

          case 'repeatS':
            if (fieldValues[0] === false) wrapS = ClampToEdgeWrapping;
            break;

          case 'repeatT':
            if (fieldValues[0] === false) wrapT = ClampToEdgeWrapping;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      if (texture) {

        texture.wrapS = wrapS;
        texture.wrapT = wrapT;

      }

      return texture;

    }

    function buildTextureTransformNode(node) {

      const transformData = {
        center: new Vector2(),
        rotation: new Vector2(),
        scale: new Vector2(),
        translation: new Vector2()
      };

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'center':
            transformData.center.set(fieldValues[0], fieldValues[1]);
            break;

          case 'rotation':
            transformData.rotation = fieldValues[0];
            break;

          case 'scale':
            transformData.scale.set(fieldValues[0], fieldValues[1]);
            break;

          case 'translation':
            transformData.translation.set(fieldValues[0], fieldValues[1]);
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      return transformData;

    }

    function buildGeometricNode(node) {

      return node.fields[0].values;

    }

    function buildWorldInfoNode(node) {

      const worldInfo = {};

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'title':
            worldInfo.title = fieldValues[0];
            break;

          case 'info':
            worldInfo.info = fieldValues;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      return worldInfo;

    }

    function buildIndexedFaceSetNode(node) {

      let color, coord, normal, texCoord;
      let ccw = true, solid = true, creaseAngle = 0;
      let colorIndex, coordIndex, normalIndex, texCoordIndex;
      let colorPerVertex = true, normalPerVertex = true;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'color':
            const colorNode = fieldValues[0];

            if (colorNode !== null) {

              color = getNode(colorNode);

            }

            break;

          case 'coord':
            const coordNode = fieldValues[0];

            if (coordNode !== null) {

              coord = getNode(coordNode);

            }

            break;

          case 'normal':
            const normalNode = fieldValues[0];

            if (normalNode !== null) {

              normal = getNode(normalNode);

            }

            break;

          case 'texCoord':
            const texCoordNode = fieldValues[0];

            if (texCoordNode !== null) {

              texCoord = getNode(texCoordNode);

            }

            break;

          case 'ccw':
            ccw = fieldValues[0];
            break;

          case 'colorIndex':
            colorIndex = fieldValues;
            break;

          case 'colorPerVertex':
            colorPerVertex = fieldValues[0];
            break;

          case 'convex':
            // field not supported
            break;

          case 'coordIndex':
            coordIndex = fieldValues;
            break;

          case 'creaseAngle':
            creaseAngle = fieldValues[0];
            break;

          case 'normalIndex':
            normalIndex = fieldValues;
            break;

          case 'normalPerVertex':
            normalPerVertex = fieldValues[0];
            break;

          case 'solid':
            solid = fieldValues[0];
            break;

          case 'texCoordIndex':
            texCoordIndex = fieldValues;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      if (coordIndex === undefined) {

        console.warn('THREE.VRMLLoader: Missing coordIndex.');

        return new BufferGeometry(); // handle VRML files with incomplete geometry definition

      }

      const triangulatedCoordIndex = triangulateFaceIndex(coordIndex, ccw);

      let colorAttribute;
      let normalAttribute;
      let uvAttribute;

      if (color) {

        if (colorPerVertex === true) {

          if (colorIndex && colorIndex.length > 0) {

            // if the colorIndex field is not empty, then it is used to choose colors for each vertex of the IndexedFaceSet.

            const triangulatedColorIndex = triangulateFaceIndex(colorIndex, ccw);
            colorAttribute = computeAttributeFromIndexedData(triangulatedCoordIndex, triangulatedColorIndex, color, 3);

          } else {

            // if the colorIndex field is empty, then the coordIndex field is used to choose colors from the Color node

            colorAttribute = toNonIndexedAttribute(triangulatedCoordIndex, new Float32BufferAttribute(color, 3));

          }

        } else {

          if (colorIndex && colorIndex.length > 0) {

            // if the colorIndex field is not empty, then they are used to choose one color for each face of the IndexedFaceSet

            const flattenFaceColors = flattenData(color, colorIndex);
            const triangulatedFaceColors = triangulateFaceData(flattenFaceColors, coordIndex);
            colorAttribute = computeAttributeFromFaceData(triangulatedCoordIndex, triangulatedFaceColors);

          } else {

            // if the colorIndex field is empty, then the color are applied to each face of the IndexedFaceSet in order

            const triangulatedFaceColors = triangulateFaceData(color, coordIndex);
            colorAttribute = computeAttributeFromFaceData(triangulatedCoordIndex, triangulatedFaceColors);


          }

        }

      }

      if (normal) {

        if (normalPerVertex === true) {

          // consider vertex normals

          if (normalIndex && normalIndex.length > 0) {

            // if the normalIndex field is not empty, then it is used to choose normals for each vertex of the IndexedFaceSet.

            const triangulatedNormalIndex = triangulateFaceIndex(normalIndex, ccw);
            normalAttribute = computeAttributeFromIndexedData(triangulatedCoordIndex, triangulatedNormalIndex, normal, 3);

          } else {

            // if the normalIndex field is empty, then the coordIndex field is used to choose normals from the Normal node

            normalAttribute = toNonIndexedAttribute(triangulatedCoordIndex, new Float32BufferAttribute(normal, 3));

          }

        } else {

          // consider face normals

          if (normalIndex && normalIndex.length > 0) {

            // if the normalIndex field is not empty, then they are used to choose one normal for each face of the IndexedFaceSet

            const flattenFaceNormals = flattenData(normal, normalIndex);
            const triangulatedFaceNormals = triangulateFaceData(flattenFaceNormals, coordIndex);
            normalAttribute = computeAttributeFromFaceData(triangulatedCoordIndex, triangulatedFaceNormals);

          } else {

            // if the normalIndex field is empty, then the normals are applied to each face of the IndexedFaceSet in order

            const triangulatedFaceNormals = triangulateFaceData(normal, coordIndex);
            normalAttribute = computeAttributeFromFaceData(triangulatedCoordIndex, triangulatedFaceNormals);

          }

        }

      } else {

        // if the normal field is NULL, then the loader should automatically generate normals, using creaseAngle to determine if and how normals are smoothed across shared vertices

        normalAttribute = computeNormalAttribute(triangulatedCoordIndex, coord, creaseAngle);

      }

      if (texCoord) {

        // texture coordinates are always defined on vertex level

        if (texCoordIndex && texCoordIndex.length > 0) {

          // if the texCoordIndex field is not empty, then it is used to choose texture coordinates for each vertex of the IndexedFaceSet.

          const triangulatedTexCoordIndex = triangulateFaceIndex(texCoordIndex, ccw);
          uvAttribute = computeAttributeFromIndexedData(triangulatedCoordIndex, triangulatedTexCoordIndex, texCoord, 2);


        } else {

          // if the texCoordIndex field is empty, then the coordIndex array is used to choose texture coordinates from the TextureCoordinate node

          uvAttribute = toNonIndexedAttribute(triangulatedCoordIndex, new Float32BufferAttribute(texCoord, 2));

        }

      }

      const geometry = new BufferGeometry();
      const positionAttribute = toNonIndexedAttribute(triangulatedCoordIndex, new Float32BufferAttribute(coord, 3));

      geometry.setAttribute('position', positionAttribute);
      geometry.setAttribute('normal', normalAttribute);

      // optional attributes

      if (colorAttribute) geometry.setAttribute('color', colorAttribute);
      if (uvAttribute) geometry.setAttribute('uv', uvAttribute);

      // "solid" influences the material so let's store it for later use

      geometry._solid = solid;
      geometry._type = 'mesh';

      return geometry;

    }

    function buildIndexedLineSetNode(node) {

      let color, coord;
      let colorIndex, coordIndex;
      let colorPerVertex = true;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'color':
            const colorNode = fieldValues[0];

            if (colorNode !== null) {

              color = getNode(colorNode);

            }

            break;

          case 'coord':
            const coordNode = fieldValues[0];

            if (coordNode !== null) {

              coord = getNode(coordNode);

            }

            break;

          case 'colorIndex':
            colorIndex = fieldValues;
            break;

          case 'colorPerVertex':
            colorPerVertex = fieldValues[0];
            break;

          case 'coordIndex':
            coordIndex = fieldValues;
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      // build lines

      let colorAttribute;

      const expandedLineIndex = expandLineIndex(coordIndex); // create an index for three.js's linesegment primitive

      if (color) {

        if (colorPerVertex === true) {

          if (colorIndex.length > 0) {

            // if the colorIndex field is not empty, then one color is used for each polyline of the IndexedLineSet.

            const expandedColorIndex = expandLineIndex(colorIndex); // compute colors for each line segment (rendering primitve)
            colorAttribute = computeAttributeFromIndexedData(expandedLineIndex, expandedColorIndex, color, 3); // compute data on vertex level

          } else {

            // if the colorIndex field is empty, then the colors are applied to each polyline of the IndexedLineSet in order.

            colorAttribute = toNonIndexedAttribute(expandedLineIndex, new Float32BufferAttribute(color, 3));

          }

        } else {

          if (colorIndex.length > 0) {

            // if the colorIndex field is not empty, then colors are applied to each vertex of the IndexedLineSet

            const flattenLineColors = flattenData(color, colorIndex); // compute colors for each VRML primitve
            const expandedLineColors = expandLineData(flattenLineColors, coordIndex); // compute colors for each line segment (rendering primitve)
            colorAttribute = computeAttributeFromLineData(expandedLineIndex, expandedLineColors); // compute data on vertex level


          } else {

            // if the colorIndex field is empty, then the coordIndex field is used to choose colors from the Color node

            const expandedLineColors = expandLineData(color, coordIndex); // compute colors for each line segment (rendering primitve)
            colorAttribute = computeAttributeFromLineData(expandedLineIndex, expandedLineColors); // compute data on vertex level

          }

        }

      }

      //

      const geometry = new BufferGeometry();

      const positionAttribute = toNonIndexedAttribute(expandedLineIndex, new Float32BufferAttribute(coord, 3));
      geometry.setAttribute('position', positionAttribute);

      if (colorAttribute) geometry.setAttribute('color', colorAttribute);

      geometry._type = 'line';

      return geometry;

    }

    function buildPointSetNode(node) {

      let color, coord;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'color':
            const colorNode = fieldValues[0];

            if (colorNode !== null) {

              color = getNode(colorNode);

            }

            break;

          case 'coord':
            const coordNode = fieldValues[0];

            if (coordNode !== null) {

              coord = getNode(coordNode);

            }

            break;


          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const geometry = new BufferGeometry();

      geometry.setAttribute('position', new Float32BufferAttribute(coord, 3));
      if (color) geometry.setAttribute('color', new Float32BufferAttribute(color, 3));

      geometry._type = 'points';

      return geometry;

    }

    function buildBoxNode(node) {

      const size = new Vector3(2, 2, 2);

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'size':
            size.x = fieldValues[0];
            size.y = fieldValues[1];
            size.z = fieldValues[2];
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const geometry = new BoxGeometry(size.x, size.y, size.z);

      return geometry;

    }

    function buildConeNode(node) {

      let radius = 1, height = 2, openEnded = false;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'bottom':
            openEnded = !fieldValues[0];
            break;

          case 'bottomRadius':
            radius = fieldValues[0];
            break;

          case 'height':
            height = fieldValues[0];
            break;

          case 'side':
            // field not supported
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const geometry = new ConeGeometry(radius, height, 16, 1, openEnded);

      return geometry;

    }

    function buildCylinderNode(node) {

      let radius = 1, height = 2;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'bottom':
            // field not supported
            break;

          case 'radius':
            radius = fieldValues[0];
            break;

          case 'height':
            height = fieldValues[0];
            break;

          case 'side':
            // field not supported
            break;

          case 'top':
            // field not supported
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const geometry = new CylinderGeometry(radius, radius, height, 16, 1);

      return geometry;

    }

    function buildSphereNode(node) {

      let radius = 1;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'radius':
            radius = fieldValues[0];
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const geometry = new SphereGeometry(radius, 16, 16);

      return geometry;

    }

    function buildElevationGridNode(node) {

      let color;
      let normal;
      let texCoord;
      let height;

      let colorPerVertex = true;
      let normalPerVertex = true;
      let solid = true;
      let ccw = true;
      let creaseAngle = 0;
      let xDimension = 2;
      let zDimension = 2;
      let xSpacing = 1;
      let zSpacing = 1;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'color':
            const colorNode = fieldValues[0];

            if (colorNode !== null) {

              color = getNode(colorNode);

            }

            break;

          case 'normal':
            const normalNode = fieldValues[0];

            if (normalNode !== null) {

              normal = getNode(normalNode);

            }

            break;

          case 'texCoord':
            const texCoordNode = fieldValues[0];

            if (texCoordNode !== null) {

              texCoord = getNode(texCoordNode);

            }

            break;

          case 'height':
            height = fieldValues;
            break;

          case 'ccw':
            ccw = fieldValues[0];
            break;

          case 'colorPerVertex':
            colorPerVertex = fieldValues[0];
            break;

          case 'creaseAngle':
            creaseAngle = fieldValues[0];
            break;

          case 'normalPerVertex':
            normalPerVertex = fieldValues[0];
            break;

          case 'solid':
            solid = fieldValues[0];
            break;

          case 'xDimension':
            xDimension = fieldValues[0];
            break;

          case 'xSpacing':
            xSpacing = fieldValues[0];
            break;

          case 'zDimension':
            zDimension = fieldValues[0];
            break;

          case 'zSpacing':
            zSpacing = fieldValues[0];
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      // vertex data

      const vertices = [];
      const normals = [];
      const colors = [];
      const uvs = [];

      for (let i = 0; i < zDimension; i++) {

        for (let j = 0; j < xDimension; j++) {

          // compute a row major index

          const index = (i * xDimension) + j;

          // vertices

          const x = xSpacing * i;
          const y = height[index];
          const z = zSpacing * j;

          vertices.push(x, y, z);

          // colors

          if (color && colorPerVertex === true) {

            const r = color[index * 3 + 0];
            const g = color[index * 3 + 1];
            const b = color[index * 3 + 2];

            colors.push(r, g, b);

          }

          // normals

          if (normal && normalPerVertex === true) {

            const xn = normal[index * 3 + 0];
            const yn = normal[index * 3 + 1];
            const zn = normal[index * 3 + 2];

            normals.push(xn, yn, zn);

          }

          // uvs

          if (texCoord) {

            const s = texCoord[index * 2 + 0];
            const t = texCoord[index * 2 + 1];

            uvs.push(s, t);


          } else {

            uvs.push(i / (xDimension - 1), j / (zDimension - 1));

          }

        }

      }

      // indices

      const indices = [];

      for (let i = 0; i < xDimension - 1; i++) {

        for (let j = 0; j < zDimension - 1; j++) {

          // from https://tecfa.unige.ch/guides/vrml/vrml97/spec/part1/nodesRef.html#ElevationGrid

          const a = i + j * xDimension;
          const b = i + (j + 1) * xDimension;
          const c = (i + 1) + (j + 1) * xDimension;
          const d = (i + 1) + j * xDimension;

          // faces

          if (ccw === true) {

            indices.push(a, c, b);
            indices.push(c, a, d);

          } else {

            indices.push(a, b, c);
            indices.push(c, d, a);

          }

        }

      }

      //

      const positionAttribute = toNonIndexedAttribute(indices, new Float32BufferAttribute(vertices, 3));
      const uvAttribute = toNonIndexedAttribute(indices, new Float32BufferAttribute(uvs, 2));
      let colorAttribute;
      let normalAttribute;

      // color attribute

      if (color) {

        if (colorPerVertex === false) {

          for (let i = 0; i < xDimension - 1; i++) {

            for (let j = 0; j < zDimension - 1; j++) {

              const index = i + j * (xDimension - 1);

              const r = color[index * 3 + 0];
              const g = color[index * 3 + 1];
              const b = color[index * 3 + 2];

              // one color per quad

              colors.push(r, g, b);
              colors.push(r, g, b);
              colors.push(r, g, b);
              colors.push(r, g, b);
              colors.push(r, g, b);
              colors.push(r, g, b);

            }

          }

          colorAttribute = new Float32BufferAttribute(colors, 3);

        } else {

          colorAttribute = toNonIndexedAttribute(indices, new Float32BufferAttribute(colors, 3));

        }

      }

      // normal attribute

      if (normal) {

        if (normalPerVertex === false) {

          for (let i = 0; i < xDimension - 1; i++) {

            for (let j = 0; j < zDimension - 1; j++) {

              const index = i + j * (xDimension - 1);

              const xn = normal[index * 3 + 0];
              const yn = normal[index * 3 + 1];
              const zn = normal[index * 3 + 2];

              // one normal per quad

              normals.push(xn, yn, zn);
              normals.push(xn, yn, zn);
              normals.push(xn, yn, zn);
              normals.push(xn, yn, zn);
              normals.push(xn, yn, zn);
              normals.push(xn, yn, zn);

            }

          }

          normalAttribute = new Float32BufferAttribute(normals, 3);

        } else {

          normalAttribute = toNonIndexedAttribute(indices, new Float32BufferAttribute(normals, 3));

        }

      } else {

        normalAttribute = computeNormalAttribute(indices, vertices, creaseAngle);

      }

      // build geometry

      const geometry = new BufferGeometry();
      geometry.setAttribute('position', positionAttribute);
      geometry.setAttribute('normal', normalAttribute);
      geometry.setAttribute('uv', uvAttribute);

      if (colorAttribute) geometry.setAttribute('color', colorAttribute);

      // "solid" influences the material so let's store it for later use

      geometry._solid = solid;
      geometry._type = 'mesh';

      return geometry;

    }

    function buildExtrusionNode(node) {

      let crossSection = [1, 1, 1, -1, -1, -1, -1, 1, 1, 1];
      let spine = [0, 0, 0, 0, 1, 0];
      let scale;
      let orientation;

      let beginCap = true;
      let ccw = true;
      let creaseAngle = 0;
      let endCap = true;
      let solid = true;

      const fields = node.fields;

      for (let i = 0, l = fields.length; i < l; i++) {

        const field = fields[i];
        const fieldName = field.name;
        const fieldValues = field.values;

        switch (fieldName) {

          case 'beginCap':
            beginCap = fieldValues[0];
            break;

          case 'ccw':
            ccw = fieldValues[0];
            break;

          case 'convex':
            // field not supported
            break;

          case 'creaseAngle':
            creaseAngle = fieldValues[0];
            break;

          case 'crossSection':
            crossSection = fieldValues;
            break;

          case 'endCap':
            endCap = fieldValues[0];
            break;

          case 'orientation':
            orientation = fieldValues;
            break;

          case 'scale':
            scale = fieldValues;
            break;

          case 'solid':
            solid = fieldValues[0];
            break;

          case 'spine':
            spine = fieldValues; // only extrusion along the Y-axis are supported so far
            break;

          default:
            console.warn('THREE.VRMLLoader: Unknown field:', fieldName);
            break;

        }

      }

      const crossSectionClosed = (crossSection[0] === crossSection[crossSection.length - 2] && crossSection[1] === crossSection[crossSection.length - 1]);

      // vertices

      const vertices = [];
      const spineVector = new Vector3();
      const scaling = new Vector3();

      const axis = new Vector3();
      const vertex = new Vector3();
      const quaternion = new Quaternion();

      for (let i = 0, j = 0, o = 0, il = spine.length; i < il; i += 3, j += 2, o += 4) {

        spineVector.fromArray(spine, i);

        scaling.x = scale ? scale[j + 0] : 1;
        scaling.y = 1;
        scaling.z = scale ? scale[j + 1] : 1;

        axis.x = orientation ? orientation[o + 0] : 0;
        axis.y = orientation ? orientation[o + 1] : 0;
        axis.z = orientation ? orientation[o + 2] : 1;
        const angle = orientation ? orientation[o + 3] : 0;

        for (let k = 0, kl = crossSection.length; k < kl; k += 2) {

          vertex.x = crossSection[k + 0];
          vertex.y = 0;
          vertex.z = crossSection[k + 1];

          // scale

          vertex.multiply(scaling);

          // rotate

          quaternion.setFromAxisAngle(axis, angle);
          vertex.applyQuaternion(quaternion);

          // translate

          vertex.add(spineVector);

          vertices.push(vertex.x, vertex.y, vertex.z);

        }

      }

      // indices

      const indices = [];

      const spineCount = spine.length / 3;
      const crossSectionCount = crossSection.length / 2;

      for (let i = 0; i < spineCount - 1; i++) {

        for (let j = 0; j < crossSectionCount - 1; j++) {

          const a = j + i * crossSectionCount;
          let b = (j + 1) + i * crossSectionCount;
          const c = j + (i + 1) * crossSectionCount;
          let d = (j + 1) + (i + 1) * crossSectionCount;

          if ((j === crossSectionCount - 2) && (crossSectionClosed === true)) {

            b = i * crossSectionCount;
            d = (i + 1) * crossSectionCount;

          }

          if (ccw === true) {

            indices.push(a, b, c);
            indices.push(c, b, d);

          } else {

            indices.push(a, c, b);
            indices.push(c, d, b);

          }

        }

      }

      // triangulate cap

      if (beginCap === true || endCap === true) {

        const contour = [];

        for (let i = 0, l = crossSection.length; i < l; i += 2) {

          contour.push(new Vector2(crossSection[i], crossSection[i + 1]));

        }

        const faces = ShapeUtils.triangulateShape(contour, []);
        const capIndices = [];

        for (let i = 0, l = faces.length; i < l; i++) {

          const face = faces[i];

          capIndices.push(face[0], face[1], face[2]);

        }

        // begin cap

        if (beginCap === true) {

          for (let i = 0, l = capIndices.length; i < l; i += 3) {

            if (ccw === true) {

              indices.push(capIndices[i + 0], capIndices[i + 1], capIndices[i + 2]);

            } else {

              indices.push(capIndices[i + 0], capIndices[i + 2], capIndices[i + 1]);

            }

          }

        }

        // end cap

        if (endCap === true) {

          const indexOffset = crossSectionCount * (spineCount - 1); // references to the first vertex of the last cross section

          for (let i = 0, l = capIndices.length; i < l; i += 3) {

            if (ccw === true) {

              indices.push(indexOffset + capIndices[i + 0], indexOffset + capIndices[i + 2], indexOffset + capIndices[i + 1]);

            } else {

              indices.push(indexOffset + capIndices[i + 0], indexOffset + capIndices[i + 1], indexOffset + capIndices[i + 2]);

            }

          }

        }

      }

      const positionAttribute = toNonIndexedAttribute(indices, new Float32BufferAttribute(vertices, 3));
      const normalAttribute = computeNormalAttribute(indices, vertices, creaseAngle);

      const geometry = new BufferGeometry();
      geometry.setAttribute('position', positionAttribute);
      geometry.setAttribute('normal', normalAttribute);
      // no uvs yet

      // "solid" influences the material so let's store it for later use

      geometry._solid = solid;
      geometry._type = 'mesh';

      return geometry;

    }

    // helper functions

    function resolveUSE(identifier) {

      const node = nodeMap[identifier];
      const build = getNode(node);

      // because the same 3D objects can have different transformations, it's necessary to clone them.
      // materials can be influenced by the geometry (e.g. vertex normals). cloning is necessary to avoid
      // any side effects

      return (build.isObject3D || build.isMaterial) ? build.clone() : build;

    }

    function parseFieldChildren(children, owner) {

      for (let i = 0, l = children.length; i < l; i++) {

        const object = getNode(children[i]);

        if (object instanceof Object3D) owner.add(object);

      }

    }

    function triangulateFaceIndex(index, ccw) {

      const indices = [];

      // since face defintions can have more than three vertices, it's necessary to
      // perform a simple triangulation

      let start = 0;

      for (let i = 0, l = index.length; i < l; i++) {

        const i1 = index[start];
        const i2 = index[i + (ccw ? 1 : 2)];
        const i3 = index[i + (ccw ? 2 : 1)];

        indices.push(i1, i2, i3);

        // an index of -1 indicates that the current face has ended and the next one begins

        if (index[i + 3] === -1 || i + 3 >= l) {

          i += 3;
          start = i + 1;

        }

      }

      return indices;

    }

    function triangulateFaceData(data, index) {

      const triangulatedData = [];

      let start = 0;

      for (let i = 0, l = index.length; i < l; i++) {

        const stride = start * 3;

        const x = data[stride];
        const y = data[stride + 1];
        const z = data[stride + 2];

        triangulatedData.push(x, y, z);

        // an index of -1 indicates that the current face has ended and the next one begins

        if (index[i + 3] === -1 || i + 3 >= l) {

          i += 3;
          start++;

        }

      }

      return triangulatedData;

    }

    function flattenData(data, index) {

      const flattenData = [];

      for (let i = 0, l = index.length; i < l; i++) {

        const i1 = index[i];

        const stride = i1 * 3;

        const x = data[stride];
        const y = data[stride + 1];
        const z = data[stride + 2];

        flattenData.push(x, y, z);

      }

      return flattenData;

    }

    function expandLineIndex(index) {

      const indices = [];

      for (let i = 0, l = index.length; i < l; i++) {

        const i1 = index[i];
        const i2 = index[i + 1];

        indices.push(i1, i2);

        // an index of -1 indicates that the current line has ended and the next one begins

        if (index[i + 2] === -1 || i + 2 >= l) {

          i += 2;

        }

      }

      return indices;

    }

    function expandLineData(data, index) {

      const triangulatedData = [];

      let start = 0;

      for (let i = 0, l = index.length; i < l; i++) {

        const stride = start * 3;

        const x = data[stride];
        const y = data[stride + 1];
        const z = data[stride + 2];

        triangulatedData.push(x, y, z);

        // an index of -1 indicates that the current line has ended and the next one begins

        if (index[i + 2] === -1 || i + 2 >= l) {

          i += 2;
          start++;

        }

      }

      return triangulatedData;

    }

    const vA = new Vector3();
    const vB = new Vector3();
    const vC = new Vector3();

    const uvA = new Vector2();
    const uvB = new Vector2();
    const uvC = new Vector2();

    function computeAttributeFromIndexedData(coordIndex, index, data, itemSize) {

      const array = [];

      // we use the coordIndex.length as delimiter since normalIndex must contain at least as many indices

      for (let i = 0, l = coordIndex.length; i < l; i += 3) {

        const a = index[i];
        const b = index[i + 1];
        const c = index[i + 2];

        if (itemSize === 2) {

          uvA.fromArray(data, a * itemSize);
          uvB.fromArray(data, b * itemSize);
          uvC.fromArray(data, c * itemSize);

          array.push(uvA.x, uvA.y);
          array.push(uvB.x, uvB.y);
          array.push(uvC.x, uvC.y);

        } else {

          vA.fromArray(data, a * itemSize);
          vB.fromArray(data, b * itemSize);
          vC.fromArray(data, c * itemSize);

          array.push(vA.x, vA.y, vA.z);
          array.push(vB.x, vB.y, vB.z);
          array.push(vC.x, vC.y, vC.z);

        }

      }

      return new Float32BufferAttribute(array, itemSize);

    }

    function computeAttributeFromFaceData(index, faceData) {

      const array = [];

      for (let i = 0, j = 0, l = index.length; i < l; i += 3, j++) {

        vA.fromArray(faceData, j * 3);

        array.push(vA.x, vA.y, vA.z);
        array.push(vA.x, vA.y, vA.z);
        array.push(vA.x, vA.y, vA.z);

      }

      return new Float32BufferAttribute(array, 3);

    }

    function computeAttributeFromLineData(index, lineData) {

      const array = [];

      for (let i = 0, j = 0, l = index.length; i < l; i += 2, j++) {

        vA.fromArray(lineData, j * 3);

        array.push(vA.x, vA.y, vA.z);
        array.push(vA.x, vA.y, vA.z);

      }

      return new Float32BufferAttribute(array, 3);

    }

    function toNonIndexedAttribute(indices, attribute) {

      const array = attribute.array;
      const itemSize = attribute.itemSize;

      const array2 = new array.constructor(indices.length * itemSize);

      let index = 0, index2 = 0;

      for (let i = 0, l = indices.length; i < l; i++) {

        index = indices[i] * itemSize;

        for (let j = 0; j < itemSize; j++) {

          array2[index2++] = array[index++];

        }

      }

      return new Float32BufferAttribute(array2, itemSize);

    }

    const ab = new Vector3();
    const cb = new Vector3();

    function computeNormalAttribute(index, coord, creaseAngle) {

      const faces = [];
      const vertexNormals = {};

      // prepare face and raw vertex normals

      for (let i = 0, l = index.length; i < l; i += 3) {

        const a = index[i];
        const b = index[i + 1];
        const c = index[i + 2];

        const face = new Face(a, b, c);

        vA.fromArray(coord, a * 3);
        vB.fromArray(coord, b * 3);
        vC.fromArray(coord, c * 3);

        cb.subVectors(vC, vB);
        ab.subVectors(vA, vB);
        cb.cross(ab);

        cb.normalize();

        face.normal.copy(cb);

        if (vertexNormals[a] === undefined) vertexNormals[a] = [];
        if (vertexNormals[b] === undefined) vertexNormals[b] = [];
        if (vertexNormals[c] === undefined) vertexNormals[c] = [];

        vertexNormals[a].push(face.normal);
        vertexNormals[b].push(face.normal);
        vertexNormals[c].push(face.normal);

        faces.push(face);

      }

      // compute vertex normals and build final geometry

      const normals = [];

      for (let i = 0, l = faces.length; i < l; i++) {

        const face = faces[i];

        const nA = weightedNormal(vertexNormals[face.a], face.normal, creaseAngle);
        const nB = weightedNormal(vertexNormals[face.b], face.normal, creaseAngle);
        const nC = weightedNormal(vertexNormals[face.c], face.normal, creaseAngle);

        vA.fromArray(coord, face.a * 3);
        vB.fromArray(coord, face.b * 3);
        vC.fromArray(coord, face.c * 3);

        normals.push(nA.x, nA.y, nA.z);
        normals.push(nB.x, nB.y, nB.z);
        normals.push(nC.x, nC.y, nC.z);

      }

      return new Float32BufferAttribute(normals, 3);

    }

    function weightedNormal(normals, vector, creaseAngle) {

      const normal = new Vector3();

      if (creaseAngle === 0) {

        normal.copy(vector);

      } else {

        for (let i = 0, l = normals.length; i < l; i++) {

          if (normals[i].angleTo(vector) < creaseAngle) {

            normal.add(normals[i]);

          }

        }

      }

      return normal.normalize();

    }

    function toColorArray(colors) {

      const array = [];

      for (let i = 0, l = colors.length; i < l; i += 3) {

        array.push(new Color(colors[i], colors[i + 1], colors[i + 2]));

      }

      return array;

    }

    /**
     * Vertically paints the faces interpolating between the
     * specified colors at the specified angels. This is used for the Background
     * node, but could be applied to other nodes with multiple faces as well.
     *
     * When used with the Background node, default is directionIsDown is true if
     * interpolating the skyColor down from the Zenith. When interpolationg up from
     * the Nadir i.e. interpolating the groundColor, the directionIsDown is false.
     *
     * The first angle is never specified, it is the Zenith (0 rad). Angles are specified
     * in radians. The geometry is thought a sphere, but could be anything. The color interpolation
     * is linear along the Y axis in any case.
     *
     * You must specify one more color than you have angles at the beginning of the colors array.
     * This is the color of the Zenith (the top of the shape).
     *
     * @param {BufferGeometry} geometry
     * @param {number} radius
     * @param {array} angles
     * @param {array} colors
     * @param {boolean} topDown - Whether to work top down or bottom up.
     */
    function paintFaces(geometry, radius, angles, colors, topDown) {

      // compute threshold values

      const thresholds = [];
      const startAngle = (topDown === true) ? 0 : Math.PI;

      for (let i = 0, l = colors.length; i < l; i++) {

        let angle = (i === 0) ? 0 : angles[i - 1];
        angle = (topDown === true) ? angle : (startAngle - angle);

        const point = new Vector3();
        point.setFromSphericalCoords(radius, angle, 0);

        thresholds.push(point);

      }

      // generate vertex colors

      const indices = geometry.index;
      const positionAttribute = geometry.attributes.position;
      const colorAttribute = new BufferAttribute(new Float32Array(geometry.attributes.position.count * 3), 3);

      const position = new Vector3();
      const color = new Color();

      for (let i = 0; i < indices.count; i++) {

        const index = indices.getX(i);
        position.fromBufferAttribute(positionAttribute, index);

        let thresholdIndexA, thresholdIndexB;
        let t = 1;

        for (let j = 1; j < thresholds.length; j++) {

          thresholdIndexA = j - 1;
          thresholdIndexB = j;

          const thresholdA = thresholds[thresholdIndexA];
          const thresholdB = thresholds[thresholdIndexB];

          if (topDown === true) {

            // interpolation for sky color

            if (position.y <= thresholdA.y && position.y > thresholdB.y) {

              t = Math.abs(thresholdA.y - position.y) / Math.abs(thresholdA.y - thresholdB.y);

              break;

            }

          } else {

            // interpolation for ground color

            if (position.y >= thresholdA.y && position.y < thresholdB.y) {

              t = Math.abs(thresholdA.y - position.y) / Math.abs(thresholdA.y - thresholdB.y);

              break;

            }

          }

        }

        const colorA = colors[thresholdIndexA];
        const colorB = colors[thresholdIndexB];

        color.copy(colorA).lerp(colorB, t);

        colorAttribute.setXYZ(index, color.r, color.g, color.b);

      }

      geometry.setAttribute('color', colorAttribute);

    }

    //

    const textureLoader = new TextureLoader(this.manager);
    textureLoader.setPath(this.resourcePath || path).setCrossOrigin(this.crossOrigin);

    // check version (only 2.0 is supported)

    if (data.indexOf('#VRML V2.0') === -1) {

      throw Error('THREE.VRMLLexer: Version of VRML asset not supported.');

    }

    // create JSON representing the tree structure of the VRML asset

    const tree = generateVRMLTree(data);

    // parse the tree structure to a three.js scene

    const scene = parseTree(tree);

    return scene;

  }

}

class VRMLLexer {

  constructor(tokens) {

    this.lexer = new chevrotain.Lexer(tokens); // eslint-disable-line no-undef

  }

  lex(inputText) {

    const lexingResult = this.lexer.tokenize(inputText);

    if (lexingResult.errors.length > 0) {

      console.error(lexingResult.errors);

      throw Error('THREE.VRMLLexer: Lexing errors detected.');

    }

    return lexingResult;

  }

}

const CstParser = chevrotain.CstParser;// eslint-disable-line no-undef

class VRMLParser extends CstParser {

  constructor(tokenVocabulary) {

    super(tokenVocabulary);

    const $ = this;

    const Version = tokenVocabulary['Version'];
    const LCurly = tokenVocabulary['LCurly'];
    const RCurly = tokenVocabulary['RCurly'];
    const LSquare = tokenVocabulary['LSquare'];
    const RSquare = tokenVocabulary['RSquare'];
    const Identifier = tokenVocabulary['Identifier'];
    const RouteIdentifier = tokenVocabulary['RouteIdentifier'];
    const StringLiteral = tokenVocabulary['StringLiteral'];
    const HexLiteral = tokenVocabulary['HexLiteral'];
    const NumberLiteral = tokenVocabulary['NumberLiteral'];
    const TrueLiteral = tokenVocabulary['TrueLiteral'];
    const FalseLiteral = tokenVocabulary['FalseLiteral'];
    const NullLiteral = tokenVocabulary['NullLiteral'];
    const DEF = tokenVocabulary['DEF'];
    const USE = tokenVocabulary['USE'];
    const ROUTE = tokenVocabulary['ROUTE'];
    const TO = tokenVocabulary['TO'];
    const NodeName = tokenVocabulary['NodeName'];

    $.RULE('vrml', function () {

      $.SUBRULE($.version);
      $.AT_LEAST_ONE(function () {

        $.SUBRULE($.node);

      });
      $.MANY(function () {

        $.SUBRULE($.route);

      });

    });

    $.RULE('version', function () {

      $.CONSUME(Version);

    });

    $.RULE('node', function () {

      $.OPTION(function () {

        $.SUBRULE($.def);

      });

      $.CONSUME(NodeName);
      $.CONSUME(LCurly);
      $.MANY(function () {

        $.SUBRULE($.field);

      });
      $.CONSUME(RCurly);

    });

    $.RULE('field', function () {

      $.CONSUME(Identifier);

      $.OR2([
        {
          ALT: function () {

            $.SUBRULE($.singleFieldValue);

          }
        },
        {
          ALT: function () {

            $.SUBRULE($.multiFieldValue);

          }
        }
      ]);

    });

    $.RULE('def', function () {

      $.CONSUME(DEF);
      $.OR([
        {
          ALT: function () {

            $.CONSUME(Identifier);

          }
        },
        {
          ALT: function () {

            $.CONSUME(NodeName);

          }
        }
      ]);

    });

    $.RULE('use', function () {

      $.CONSUME(USE);
      $.OR([
        {
          ALT: function () {

            $.CONSUME(Identifier);

          }
        },
        {
          ALT: function () {

            $.CONSUME(NodeName);

          }
        }
      ]);

    });

    $.RULE('singleFieldValue', function () {

      $.AT_LEAST_ONE(function () {

        $.OR([
          {
            ALT: function () {

              $.SUBRULE($.node);

            }
          },
          {
            ALT: function () {

              $.SUBRULE($.use);

            }
          },
          {
            ALT: function () {

              $.CONSUME(StringLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(HexLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(NumberLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(TrueLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(FalseLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(NullLiteral);

            }
          }
        ]);


      });

    });

    $.RULE('multiFieldValue', function () {

      $.CONSUME(LSquare);
      $.MANY(function () {

        $.OR([
          {
            ALT: function () {

              $.SUBRULE($.node);

            }
          },
          {
            ALT: function () {

              $.SUBRULE($.use);

            }
          },
          {
            ALT: function () {

              $.CONSUME(StringLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(HexLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(NumberLiteral);

            }
          },
          {
            ALT: function () {

              $.CONSUME(NullLiteral);

            }
          }
        ]);

      });
      $.CONSUME(RSquare);

    });

    $.RULE('route', function () {

      $.CONSUME(ROUTE);
      $.CONSUME(RouteIdentifier);
      $.CONSUME(TO);
      $.CONSUME2(RouteIdentifier);

    });

    this.performSelfAnalysis();

  }

}

class Face {

  constructor(a, b, c) {

    this.a = a;
    this.b = b;
    this.c = c;
    this.normal = new Vector3();

  }

}

const TEXTURE_TYPE = {
  INTENSITY: 1,
  INTENSITY_ALPHA: 2,
  RGB: 3,
  RGBA: 4
};

export {VRMLLoader};
